package org.ii.infrastructure.tiles.view;

/*
 * Copyright 2004-2008 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.tiles.Attribute;
import org.apache.tiles.Definition;
import org.apache.tiles.Attribute.AttributeType;
import org.apache.tiles.access.TilesAccess;
import org.apache.tiles.context.ChainedTilesRequestContextFactory;
import org.apache.tiles.context.TilesRequestContext;
import org.apache.tiles.context.TilesRequestContextFactory;
import org.apache.tiles.definition.DefinitionsFactoryException;
import org.apache.tiles.impl.BasicTilesContainer;
import org.springframework.js.ajax.AjaxHandler;
import org.springframework.js.ajax.SpringJavascriptAjaxHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.support.JstlUtils;
import org.springframework.web.servlet.support.RequestContext;

/**
 * This is a copy of Spring Webflows AjaxTilesView with some changes.
 * <p/>
 * We have made a custom version of Springs TilesView to make the integration with Tiles work, see {@link TilesView}. This
 * AjaxTilesView is extending our TilesView.
 * <p/>
 * Some changes have also been done to make things work with Tiles 2.2.1. See the code for further descriptions.
 * <p/>
 * 
 * @see org.springframework.js.ajax.tiles2.AjaxTilesView
 * @future tonje.broden 2009 11 11 Extend Springs TilesView when Spring has made the necessary changes to integrate with Tiles
 * 2.2.1.
 * @future tonje.broden 2009 11 11 If a newer version of Spring Webflow works with Tiles 2.2.1 use Spring Webflows AjaxTilesView
 * instead.
 * @see org.springframework.js.ajax.tiles2.AjaxTilesView <p>
 * Waiting for updates on spring-webflow (includes spring-js). As of now we use spring-webflow 2.0.8.RELEASED. Waiting for newer
 * version, preferably 3.0.0
 * </p>
 * @author tonje.broden 2009 11 11, @author mattis 2009 11 16
 */
@SuppressWarnings("deprecation")
public class AjaxTilesView extends TilesView {
	private static final String FRAGMENTS_PARAM = "fragments";

	private TilesRequestContextFactory tilesRequestContextFactory;

	private AjaxHandler ajaxHandler = new SpringJavascriptAjaxHandler();

	private final String DEFINITION = AttributeType.DEFINITION.toString();

	private final String TEMPLATE = AttributeType.TEMPLATE.toString();

	private final String STRING = AttributeType.STRING.toString();

	private final String OBJECT = AttributeType.OBJECT.toString();

	private final static String VELOCITY = "velocity";

	@Override
	/*
	 * TODO tonje.broden 2009 11 11 It is no longer possible to retrieve the tilesRequestContextFactory form the container. (see
	 * todo below) This way was found in an example on the net. Is it safe? (See also to do further down)
	 */
	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		if (tilesRequestContextFactory == null) {
			tilesRequestContextFactory = new ChainedTilesRequestContextFactory();
			tilesRequestContextFactory.init(new HashMap<String, String>());
		}
	}

	public AjaxHandler getAjaxHandler() {
		return ajaxHandler;
	}

	public void setAjaxHandler(final AjaxHandler ajaxHandler) {
		this.ajaxHandler = ajaxHandler;
	}

	@Override
	protected void renderMergedTemplateModel(final Map<String, Object> model, final HttpServletRequest request, final HttpServletResponse response)
	throws Exception {
		final ServletContext servletContext = getServletContext();
		final boolean isAjax = ajaxHandler.isAjaxRequest(request, response);
		if (isAjax) {
			final String[] attrNames = getRenderFragments(model, request, response);
			if (attrNames.length == 0) {
				logger.warn("An Ajax request was detected, but no fragments were specified to be re-rendered.  "
						+ "Falling back to full page render.  This can cause unpredictable results when processing " + "the ajax response on the client.");
				super.renderMergedTemplateModel(model, request, response);
				return;
			}
			final BasicTilesContainer container = (BasicTilesContainer)TilesAccess.getContainer(servletContext);
			if (container == null) {
				throw new ServletException("Tiles container is not initialized. " + "Have you added a TilesConfigurer to your web application context?");
			}
			exposeModelAsRequestAttributes(model, request);
			JstlUtils.exposeLocalizationContext(new RequestContext(request, servletContext));
			/*
			 * TODO tonje.broden 2009 11 11 since getContextFactory() is deprecated and now returns null the retrieval of the
			 * tilesRequestContext must be done in a different way. The new way used here is taken from an example found on the
			 * net. Is it safe? (See also to do further up)
			 */
			/*
			 * TilesRequestContext tilesRequestContext = container.getContextFactory().createRequestContext(
			 * container.getApplicationContext(), new Object[] { request, response });
			 */
			final TilesRequestContext tilesRequestContext = tilesRequestContextFactory.createRequestContext(container.getApplicationContext(),
					new Object[]{request, response});
			final Definition compositeDefinition = container.getDefinitionsFactory().getDefinition(getUrl(), tilesRequestContext);
			final Map<String, Attribute> flattenedAttributeMap = new HashMap<String, Attribute>();
			flattenAttributeMap(container, tilesRequestContext, flattenedAttributeMap, compositeDefinition, request, response);
			/*
			 * initialize the session before rendering any fragments. Otherwise views that require the session which has not
			 * otherwise been initialized will fail to render
			 */
			request.getSession();
			response.flushBuffer();
			final Object[] requestItems = new Object[]{request, response};
			/*
			 * Bugfix: Must add missing attributes to attribute context. Problems occurs e.g. when rendering a fragment (template)
			 * containing cascaded tiles definitions. Missing cascaded attributes is currently only a problem when rendering
			 * templates.
			 */
			container.getAttributeContext(requestItems).addMissing(flattenedAttributeMap);
			for (final String attrName : attrNames) {
				final Attribute attributeToRender = flattenedAttributeMap.get(attrName);
				if (attributeToRender == null) {
					throw new ServletException("No tiles attribute with a name of '" + attrName + "' could be found for the current view: " + this);
				} else {
					container.render(attributeToRender, requestItems);
				}
			}
		} else {
			super.renderMergedTemplateModel(model, request, response);
		}
	}

	@SuppressWarnings("unchecked")
	protected String[] getRenderFragments(final Map model, final HttpServletRequest request, final HttpServletResponse response) {
		final String attrName = request.getParameter(FRAGMENTS_PARAM);
		final String[] renderFragments = StringUtils.commaDelimitedListToStringArray(attrName);
		return StringUtils.trimArrayElements(renderFragments);
	}

	/**
	 * Flatten attribute Map. Custom implementation of protected method in
	 * {@link org.springframework.js.ajax.tiles2.AjaxTilesView}. Custom handling of velocity type and cascaded attributes.
	 * <p/>
	 * 
	 * @see org.springframework.js.ajax.tiles2.AjaxTilesView
	 * @param container
	 * @param requestContext
	 * @param resultMap
	 * @param compositeDefinition
	 * @param request
	 * @param response
	 * @throws Exception
	 * @author mattis 2009 11 16
	 */
	protected void flattenAttributeMap(final BasicTilesContainer container, final TilesRequestContext requestContext, final Map<String, Attribute> resultMap,
			final Definition compositeDefinition, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
		if ( (compositeDefinition.getLocalAttributeNames() != null) && (compositeDefinition.getLocalAttributeNames().size() > 0)) {
			final Set<String> attributeNames = compositeDefinition.getLocalAttributeNames();
			for (final String key : attributeNames) {
				final Attribute attr = compositeDefinition.getAttribute(key);
				String attrType = attr.getRenderer();
				if (attrType == null) {
					attrType = detectType(container, requestContext, attr);
				}
				if (DEFINITION.equals(attrType) || TEMPLATE.equals(attrType) || VELOCITY.equals(attrType)) {
					resultMap.put(key, attr);
					if (DEFINITION.equals(attrType)) {
						final Definition nestedDefinition = container.getDefinitionsFactory().getDefinition(attr.getValue().toString(), requestContext);
						if ( (nestedDefinition != null) && (nestedDefinition != compositeDefinition)) {
							flattenAttributeMap(container, requestContext, resultMap, nestedDefinition, request, response);
						}
					}
				}
			}
		}
		/* Add cascaded attributes to resultmap */
		if ( (compositeDefinition != null) && (compositeDefinition.getCascadedAttributeNames() != null)) {
			for (final String key : compositeDefinition.getCascadedAttributeNames()) {
				resultMap.put(key, compositeDefinition.getCascadedAttribute(key));
			}
		}
		/*
		 * Removed unused code (Process dynamic attributes below). I'm not certain what it is supposed to do, but I imagine it
		 * could be used when adding new tiles runtime (?). If this is ever attempted - reinsert this code and go from there.
		 */
		// 		Process dynamic attributes
		//		final AttributeContext attributeContext = container.getAttributeContext(new Object[]{request, response});
		//		for (final Iterator i = attributeContext.getAttributeNames(); i.hasNext();) {
		//			final String key = (String)i.next();
		//			final Attribute attr = attributeContext.getAttribute(key);
		//			resultMap.put(key, attr);
		//		}
	}

	private String detectType(final BasicTilesContainer container, final TilesRequestContext requestContext, final Attribute attr)
	throws DefinitionsFactoryException {
		if (attr.getValue() instanceof String) {
			if (container.getDefinitionsFactory().getDefinition(attr.getValue().toString(), requestContext) != null) {
				return DEFINITION;
			} else if (attr.getValue().toString().startsWith("/")) {
				return TEMPLATE;
			} else {
				return STRING;
			}
		}
		return OBJECT;
	}
}
